<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Latency & Throughput Comparison</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- Tippy.js -->
    <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css">
    <script src="https://unpkg.com/@popperjs/core@2"></script>
    <script src="https://unpkg.com/tippy.js@6"></script>
    <style>
        :root {
            --bg: #000000;
            --text: #FFFFFF;

            --brand-yellow: #EFFB5E;
            --accent-cyan: #71BFED;
            --accent-magenta: #E373EC;
            --accent-lime: #70E350;
            --accent-orange: #DB7B40;

            --grid-light: #DADAD9;
            --grid-mid: #9FA19C;
        }

        body {
            margin: 0;
            font-family: system-ui, sans-serif;
            background: var(--bg);
            color: var(--text);
        }

        header {
            display: flex;
            gap: 1rem;
            align-items: center;
            padding: 10px 14px;
            flex-wrap: wrap;
            position: relative;
            padding-right: 260px; /* reserve space so legend doesn't overlap controls */
        }

        select {
            background: var(--bg);
            color: var(--text);
            border: 1px solid var(--grid-mid);
            padding: 4px 6px;
        }

        .legend {
            position: fixed;
            top: 8px;
            right: 14px;
            z-index: 10;
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            padding: 4px 8px;
            background: rgba(0, 0, 0, 0.55);
            border: 1px solid var(--grid-mid);
            border-radius: 4px;
        }

        .legend-item {
            display: flex;
            align-items: center;
            gap: 4px;
            font-size: 12px;
        }

        .legend-swatch {
            width: 16px;
            height: 14px;
            border: 1px solid var(--grid-mid);
        }

        .panes {
            display: flex;
            flex-wrap: wrap;
        }

        .pane {
            flex: 1 1 620px;
            min-width: 480px;
            border: none;
        }

        .pane-header {
            padding: 6px 10px;
            font-weight: 600;
            font-size: 14px;
            background: rgba(255, 255, 255, 0.04);
            display: flex;
            justify-content: center;
            position: relative;
        }

        .pane-header .status {
            position: absolute;
            right: 10px;
            top: 6px;
        }

        .charts {
            display: flex;
            flex-direction: column;
            gap: 12px;
            padding: 6px 10px 24px;
        }

        svg {
            width: 100%;
            overflow: visible;
            font-size: 11px;
        }

        .axis path,
        .axis line {
            stroke: var(--grid-mid);
        }

        .axis text {
            fill: var(--text);
        }

        .bar-seg {
            stroke: none;
        }

        .qps-dot {
            fill: var(--brand-yellow);
        }

        .qps-line {
            stroke: var(--brand-yellow);
        }

        .status {
            font-size: 11px;
            color: var(--grid-mid);
            margin-left: 8px;
        }

        .grid-line {
            stroke: var(--grid-light);
            stroke-width: 1;
            opacity: .25;
        }

        /* Tippy theme */
        .tippy-box[data-theme~='ch-dark'] {
            background: var(--bg);
            color: var(--text);
            border: 1px solid var(--grid-mid);
            font-size: 12px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
        }

        .tippy-box[data-theme~='ch-dark'] .tippy-content {
            padding: 6px 8px;
            line-height: 1.25;
        }
    </style>
</head>

<body>
    <header>
        <label for="querySelect">Query:</label>
        <select id="querySelect"></select>
        <span id="info"></span>
    </header>
    <div id="legend" class="legend"></div>
    <div class="panes">
        <div class="pane" id="pane-left">
            <div class="pane-header">
                <select id="left-dataset"></select>
                <span class="status" id="left-status"></span>
            </div>
            <div class="charts">
                <svg id="left-latency"></svg>
            </div>
        </div>
        <div class="pane" id="pane-right">
            <div class="pane-header">
                <select id="right-dataset"></select>
                <span class="status" id="right-status"></span>
            </div>
            <div class="charts">
                <svg id="right-latency"></svg>
            </div>
        </div>
    </div>
    <script>
        // (() => {
            'use strict';
            // ---------------------- Config ----------------------
            const DATASET_FILES = [
                "no-concurrency-control.json",
                "concurrency-control.json"
            ];
            const REQUIRED_POINTS = 32; // minimum required data rows (after filtering invalid rows)
            const PCTS = ["0", "10", "20", "30", "40", "50", "60", "70", "80", "90", "95", "99", "99.9", "99.99"];
            const DRAW_PCTS = PCTS.filter(p => parseFloat(p) <= 99 && parseFloat(p) != 95);

            // New: derived constants and formatters
            const TOP_PCT = DRAW_PCTS[DRAW_PCTS.length - 1];
            const fmtQps = d3.format(",.1f");
            const fmtMs = d3.format(".3f");
            const fmtMsAxis = d3.format(",.0f"); // axis-friendly ms

            // Build colorScale using CSS variables (single source of color truth)
            const css = getComputedStyle(document.documentElement);
            const colorScale = d3.scaleLinear()
                .domain([0, 0.25, 0.5, 0.75, 1])
                .range([
                    css.getPropertyValue('--bg').trim(),
                    css.getPropertyValue('--accent-lime').trim(),
                    css.getPropertyValue('--brand-yellow').trim(),
                    css.getPropertyValue('--accent-orange').trim(),
                    css.getPropertyValue('--accent-magenta').trim()
                ]);

            const margin = { top: 30, right: 70, bottom: 44, left: 70 };
            const latencyHeight = 340; // unified chart height

            // ---------------------- State -----------------------
            let datasetData = {};
            let leftDatasetId = null;
            let rightDatasetId = null;
            let currentKey = null;

            // ---------------------- Legend ----------------------
            function buildLegend() {
                const lg = d3.select("#legend").html("");
                // Skip P0 in the legend (we don't render a P0 segment)
                DRAW_PCTS.filter(p => p !== "0").forEach(p => {
                    lg.append("div").attr("class", "legend-item")
                        .html(`<div class="legend-swatch" style="background:${colorScale(parseFloat(p) / 100)}"></div>P${p}`);
                });
            }

            // ---------------------- Data Prep -------------------
            function prepareRows(rows) {
                if (!rows) return [];
                return rows
                    .filter(r => r[0] > 0)
                    .sort((a, b) => d3.ascending(a[0], b[0]))
                    .map(r => {
                        const conc = r[0];
                        const qps = r[1];
                        const percentiles = {};
                        PCTS.forEach((p, i) => {
                            let v = r[2 + i];
                            // convert seconds -> milliseconds
                            if (v != null) v = v * 1000;
                            percentiles[p] = v;
                        });
                        return { conc, qps, percentiles };
                    });
            }
            function getDataForKey(raw, key) { return prepareRows(raw[key]); }

            // Helper: strip ".log" suffix from keys
            function normalizeKeys(obj) {
                const out = {};
                Object.keys(obj || {}).forEach(k => {
                    const nk = k.replace(/\.log$/, '');
                    out[nk] = obj[k];
                });
                return out;
            }

            // ---------------------- Rendering -------------------
            function renderAll() {
                if (!leftDatasetId || !rightDatasetId) return;
                const leftDataRaw = datasetData[leftDatasetId]?.raw || {};
                const rightDataRaw = datasetData[rightDatasetId]?.raw || {};
                if (!currentKey) return;

                const leftData = getDataForKey(leftDataRaw, currentKey);
                const rightData = getDataForKey(rightDataRaw, currentKey);

                const latencyMax = d3.max([
                    leftData.length ? d3.max(leftData, d => d.percentiles[TOP_PCT]) : 0,
                    rightData.length ? d3.max(rightData, d => d.percentiles[TOP_PCT]) : 0
                ]) || 1;
                const qpsMax = d3.max([
                    leftData.length ? d3.max(leftData, d => d.qps) : 0,
                    rightData.length ? d3.max(rightData, d => d.qps) : 0
                ]) || 1;

                const leftStatus = datasetData[leftDatasetId]?.status || "ok";
                const rightStatus = datasetData[rightDatasetId]?.status || "ok";

                renderPane({
                    key: currentKey, raw: leftDataRaw,
                    svg: d3.select("#left-latency"),
                    statusSel: d3.select("#left-status"),
                    latencyMax, qpsMax, statusStr: leftStatus
                });
                renderPane({
                    key: currentKey, raw: rightDataRaw,
                    svg: d3.select("#right-latency"),
                    statusSel: d3.select("#right-status"),
                    latencyMax, qpsMax, statusStr: rightStatus
                });
            }

            function renderPane({ key, raw, svg, statusSel, latencyMax, qpsMax, statusStr }) {
                const data = getDataForKey(raw, key);
                if (!data.length) {
                    svg.selectAll("*").remove();
                    statusSel.text(statusStr === "error" ? "Load failed" : "No data");
                    return;
                }
                statusSel.text(statusStr === "error" ? "Load failed" : "");

                const x = d3.scaleBand()
                    .domain(data.map(d => d.conc))
                    .paddingInner(0.2)
                    .paddingOuter(0.05);

                function resize() {
                    const width = svg.node().clientWidth;
                    x.range([margin.left, width - margin.right]);
                    drawCombined(svg, data, x, width, key, latencyMax, qpsMax);
                }

                // Removed per-pane resize listener; rely on global renderAll
                resize();
            }

            function drawCombined(svg, data, x, width, key, latencyMax, qpsMax) {
                svg
                    .attr("height", latencyHeight + margin.top + margin.bottom)
                    .selectAll("*").remove();

                // Scales
                const yLat = d3.scaleLinear()
                    .domain([0, latencyMax]).nice()
                    .range([latencyHeight + margin.top, margin.top]);

                const yQps = d3.scaleLinear()
                    .domain([0, qpsMax]).nice()
                    .range([latencyHeight + margin.top, margin.top]);

                // Grid lines (QPS axis)
                svg.append("g")
                    .selectAll("line")
                    .data(yQps.ticks(10))
                    .join("line")
                    .attr("class", "grid-line")
                    .attr("x1", margin.left)
                    .attr("x2", width - margin.right)
                    .attr("y1", d => yQps(d))
                    .attr("y2", d => yQps(d));

                // X axis
                svg.append("g")
                    .attr("class", "axis")
                    .attr("transform", `translate(0,${latencyHeight + margin.top})`)
                    .call(d3.axisBottom(x).tickSizeOuter(0));

                // Left axis: QPS
                svg.append("g")
                    .attr("class", "axis")
                    .attr("transform", `translate(${margin.left},0)`)
                    .call(d3.axisLeft(yQps).ticks(10).tickFormat(fmtQps));

                // Right axis: Latency (ms)
                svg.append("g")
                    .attr("class", "axis")
                    .attr("transform", `translate(${width - margin.right},0)`)
                    .call(d3.axisRight(yLat).ticks(10).tickFormat(d => fmtMsAxis(d)));

                // Title
                svg.append("text")
                    .attr("x", width / 2)
                    .attr("y", 18)
                    .attr("fill", "var(--text)")
                    .attr("font-weight", "bold")
                    .attr("text-anchor", "middle")
                    .text(`${key} performance`);

                // Axis labels
                svg.append("text")
                    .attr("x", margin.left - 50)
                    .attr("y", margin.top - 18)
                    .attr("fill", "var(--grid-mid)")
                    .attr("font-size", "10px")
                    .text("Throughput, QPS");

                svg.append("text")
                    .attr("x", width - margin.right + 40)
                    .attr("y", margin.top - 18)
                    .attr("fill", "var(--grid-mid)")
                    .attr("font-size", "10px")
                    .attr("text-anchor", "end")
                    .text("Latency, ms");

                // X-axis label (moved down)
                svg.append("text")
                    .attr("x", width / 2)
                    .attr("y", latencyHeight + margin.top + 36)
                    .attr("fill", "var(--grid-mid)")
                    .attr("font-size", "10px")
                    .attr("text-anchor", "middle")
                    .text("Concurrent queries");

                // Latency stacked percentile bars (ms)
                const barWidth = x.bandwidth();
                const g = svg.append("g");
                data.forEach(d => {
                    const base = d.percentiles[DRAW_PCTS[0]];
                    if (base == null) return; // guard missing base percentile

                    let prev = base;
                    DRAW_PCTS.slice(1).forEach(p => {
                        const val = d.percentiles[p];
                        if (val == null || val < prev) return;
                        const h = yLat(prev) - yLat(val);
                        if (h <= 0) { prev = val; return; }
                        g.append("rect")
                            .attr("class", "bar-seg")
                            .attr("x", x(d.conc))
                            .attr("width", barWidth)
                            .attr("y", yLat(val))
                            .attr("height", h)
                            .attr("fill", colorScale(parseFloat(p) / 100))
                            .attr("data-tippy-content",
                                `Concurrency: ${d.conc}<br>P${p}: ${fmtMs(val)} ms`
                            );
                        prev = val;
                    });
                });

                // QPS line + dots
                const bw = x.bandwidth();
                const line = d3.line()
                    .x(d => x(d.conc) + bw / 2)
                    .y(d => yQps(d.qps))
                    .curve(d3.curveMonotoneX);

                svg.append("path")
                    .attr("fill", "none")
                    .attr("class", "qps-line")
                    .attr("stroke-width", 2)
                    .attr("d", line(data));

                svg.selectAll(".qps-dot")
                    .data(data)
                    .join("circle")
                    .attr("class", "qps-dot")
                    .attr("cx", d => x(d.conc) + bw / 2)
                    .attr("cy", d => yQps(d.qps))
                    .attr("r", 3)
                    .attr("data-tippy-content", d =>
                        `Concurrency: ${d.conc}<br>QPS: ${fmtQps(d.qps)}`
                    );

                // Initialize a single delegated Tippy per SVG (avoids per-element instances)
                if (window.tippy && !svg.node().__tippyDelegated) {
                    tippy.delegate(svg.node(), {
                        target: '[data-tippy-content]',
                        allowHTML: true,
                        theme: 'ch-dark',
                        animation: 'none',
                        appendTo: document.body,
                        maxWidth: 320,
                        delay: [0, 0],
                        duration: [0, 0]
                    });
                    svg.node().__tippyDelegated = true;
                }
            }

            // ---------------------- Dataset / Query Selectors ---------------
            function datasetIdFromPath(p) {
                const parts = p.split("/").filter(Boolean);
                return parts.length >= 2 ? parts[parts.length - 2] : p.replace(".json", "");
            }
            function buildDatasetSelects() {
                const ids = Object.keys(datasetData);
                const leftSel = d3.select("#left-dataset").html("");
                const rightSel = d3.select("#right-dataset").html("");
                ids.forEach(id => {
                    leftSel.append("option").attr("value", id).text(id);
                    rightSel.append("option").attr("value", id).text(id);
                });
                if (!leftDatasetId) {
                    // Prefer specific defaults if present
                    leftDatasetId = ids.find(i => i === "no-control") || ids[0];
                    rightDatasetId = ids.find(i => i === "concurrency-control-16") || ids[1] || ids[0];
                }
                leftSel.property("value", leftDatasetId);
                rightSel.property("value", rightDatasetId);
                leftSel.on("change", e => { leftDatasetId = e.target.value; updateQueries(); });
                rightSel.on("change", e => { rightDatasetId = e.target.value; updateQueries(); });
            }
            function updateQueries() {
                const leftRaw = datasetData[leftDatasetId]?.raw || {};
                const rightRaw = datasetData[rightDatasetId]?.raw || {};
                const leftKeys = Object.keys(leftRaw);
                const rightKeys = Object.keys(rightRaw);
                let keys = leftKeys.filter(k => rightKeys.includes(k));
                if (!keys.length) keys = Array.from(new Set([...leftKeys, ...rightKeys]));
                keys.sort((a, b) => {
                    const na = parseInt(a.replace(/[^\d]/g, ''), 10);
                    const nb = parseInt(b.replace(/[^\d]/g, ''), 10);
                    return (isNaN(na) || isNaN(nb)) ? d3.ascending(a, b) : d3.ascending(na, nb);
                });
                populateSelect(keys);
            }
            function populateSelect(keys) {
                const sel = d3.select("#querySelect").html("");
                keys.forEach(k => sel.append("option").attr("value", k).text(k));
                sel.on("change", e => { currentKey = e.target.value; renderAll(); });
                if (!keys.length) {
                    currentKey = null;
                    renderAll();
                    return;
                }
                if (!currentKey || !keys.includes(currentKey)) currentKey = keys[0];
                sel.property("value", currentKey);
                renderAll();
            }

            // ---------------------- Init ------------------------
            function init() {
                buildLegend();
                Promise.allSettled(
                    DATASET_FILES.map(url => fetch(url).then(r => r.json()))
                ).then(results => {
                    results.forEach((res, idx) => {
                        const url = DATASET_FILES[idx];
                        const id = datasetIdFromPath(url);
                        if (res.status === "fulfilled") {
                            let rawAll = normalizeKeys(res.value);
                            // Filter: drop keys with fewer than REQUIRED_POINTS valid rows.
                            const filtered = {};
                            Object.keys(rawAll).forEach(k => {
                                const rows = Array.isArray(rawAll[k]) ? rawAll[k] : [];
                                // count only rows we would later keep (r[0] > 0)
                                const validCount = rows.filter(r => r && r[0] > 0).length;
                                if (validCount >= REQUIRED_POINTS) filtered[k] = rawAll[k];
                            });
                            // Fallback: if everything filtered out, keep original so UI not empty.
                            const raw = Object.keys(filtered).length ? filtered : rawAll;
                            datasetData[id] = { url, raw, status: "ok" };
                        } else {
                            datasetData[id] = { url, raw: {}, status: "error" };
                        }
                    });
                    buildDatasetSelects();
                    updateQueries();
                    window.addEventListener("resize", renderAll);
                    // Status text now handled dynamically in renderPane
                }).catch(err => {
                    d3.select("#info").text("Init error: " + err);
                });
            }
            init();
        // })(); // end IIFE
    </script>
</body>

</html>
